/* EasyWay Game Engine
 * Copyright (C) 2006 Daniele Paggi.
 *  
 * Written by: 2006 Daniele Paggi<dshnt@hotmail.com>
 *   
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Library General Public License for more details.
 * 
 * You should have received a copy of the GNU Library General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.easyway.input;

import java.awt.Dimension;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import java.util.ArrayList;

import org.easyway.gui.base.IContainer;
import org.easyway.interfaces.base.IBaseObject;
import org.easyway.interfaces.base.ITexture;
import org.easyway.interfaces.extended.IDrawing;
import org.easyway.interfaces.sprites.IClickable;
import org.easyway.interfaces.sprites.IMaskerable;
import org.easyway.interfaces.sprites.IPlain2D;
import org.easyway.lists.BaseList;
import org.easyway.objects.Camera;
import org.easyway.objects.sprites2D.Mask;
import org.easyway.system.Core;
import org.easyway.system.StaticRef;
import org.easyway.utils.Utility;
import org.lwjgl.LWJGLException;
import org.lwjgl.input.Cursor;

/**
 * this class manages the mouse inputs.<br>
 */
public class Mouse {
	public static final boolean useLwjgl = Core.useLwjgl;

	/** buffered coordiantes */
	protected static int bx, by;

	/** buffered button status */
	protected static boolean bleft, bright, bmiddle;

	/** x coordinate */
	protected static int x;

	/** y coordinate */
	protected static int y;

	/** x coordinate */
	protected static int oldx;

	/** y coordinate */
	protected static int oldy;

	protected static boolean oldSomeOne, someOne;

	/** indicates if the left button is down or not */
	protected static boolean left, oldLeft;

	/** indicates if the right button is down or not */
	protected static boolean right, oldRight;

	/** indicates if the middle button is down or not */
	protected static boolean middle, oldMiddle;

	// protected static ArrayList<__Assocation> objects = new
	// ArrayList<__Assocation>(
	// 100);

	protected static EWMouseList objects = new EWMouseList();

	protected static EWMouseList guiObjects = new EWMouseList();

	protected static EWMouseList lastGuiObjs = new EWMouseList();

	protected static boolean guiMode = false;
	
	public static boolean collisionMode = false;

	/** data loop of mouse: updates the x,y coordiantes and the button status */
	public synchronized static void loop() {
		if (!org.lwjgl.input.Mouse.isCreated())
			return;
		oldx = x;
		oldy = y;
		// if (useLwjgl) {
		x = org.lwjgl.input.Mouse.getX();
		y = StaticRef.core.getHeight() - org.lwjgl.input.Mouse.getY() - 1;
		// System.out.println("x "+x+" y "+y);
		oldLeft = left;
		oldRight = right;
		oldMiddle = middle;

		left = org.lwjgl.input.Mouse.isButtonDown(0);
		right = org.lwjgl.input.Mouse.isButtonDown(1);
		middle = org.lwjgl.input.Mouse.isButtonDown(2);
		// } else {
		// x = bx;
		// y = by;
		// left = bleft;
		// right = bright;
		// middle = bmiddle;
		// }
		oldSomeOne = someOne;
		someOne = left || right || middle;

		guiMode = false;
		collisionMode = true;
		loopGuiObject();
		if (!guiMode) {
			loopObject();
		}
		collisionMode = false;

	}

	protected static void loopObject() {
		objects.startScan();
		EWMouseAssociation obj;
		while (objects.next()) {
			obj = objects.getCurrent();
			if (obj == null || obj.object == null)
				continue;
			if (testCollision(obj.object)) {
				if (!obj.over)
					over(obj);
				int nx = getX() - (int) obj.object.getXOnScreen();
				int ny = getY() - (int) obj.object.getYOnScreen();
				obj.object.onOver(nx, ny);
				if (someOne) {
					obj.object.onDown(nx, ny);
					obj.down = true;
					if (oldSomeOne && (oldx != x || oldy != y)) {
						obj.object.onDrag(x - oldx, y - oldy);
					}
				}
				if (oldSomeOne && !someOne) {
					if (obj.down) {
						obj.down = false;
						obj.object.onClick(nx, ny);
					}
					obj.object.onRelease(nx, ny);
				}
				// obj.object.onClick(nx, ny);
			} else if (obj.over) {
				notOver(obj);
			}
		}
	}

	protected static void loopGuiObject() {
		EWMouseAssociation obj;
		IContainer container;

		for (int i=0; i<guiObjects.size(); ++i ) {
			// start test object
			obj = guiObjects.get(i);
			if (obj == null || obj.object == null)
				continue;
			if (testCollision(obj.object)) {
				assert obj.object instanceof IContainer;
				container = searchChild((IContainer) obj.object);

				testGuiObj(container.getMouseAssociation());
				break;
			}
			// end test object
		}
		lastGuiObjs.startScan();
		while( lastGuiObjs.next() ) {
			obj = lastGuiObjs.getCurrent();
			if (obj == null || obj.object == null ) {
				//lastGuiObjs.remove(obj);
				continue;
			}
			if (!testCollision(obj.object)) {
				notOver(obj);
				lastGuiObjs.remove(obj);
			}
		}
	}

	protected static IContainer searchChild(IContainer father) {

		IContainer child;
		for (int i = 0; i < father.getChildren().size(); ++i) {
			child = father.getChildren().get(i);
			if (child == null) {
				Utility.error("null child", new Exception("null child"));
				continue;
			}
			if (testCollision(child)) {
				return searchChild(child);
			}
		}

		return father;
	}

	protected static void testGuiObj(EWMouseAssociation obj) {
		if (!obj.getUsedLists().contains(lastGuiObjs)) {
			lastGuiObjs.add(obj);
		}
		// if (testCollision(obj.object)) {
		if (!obj.over)
			over(obj);
		int nx = getX() - (int) obj.object.getXOnScreen();
		int ny = getY() - (int) obj.object.getYOnScreen();
		obj.object.onOver(nx, ny);
		if (someOne) {
			obj.object.onDown(nx, ny);
			obj.down = true;
			if (oldSomeOne && (oldx != x || oldy != y)) {
				obj.object.onDrag(x - oldx, y - oldy);
			}
		}
		if (oldSomeOne && !someOne) {
			if (obj.down) {
				obj.down = false;
				obj.object.onClick(nx, ny);
			}
			obj.object.onRelease(nx, ny);
		}
		// obj.object.onClick(nx, ny);
		// } else if (obj.over) {
		// notOver(obj);
		// }
	}

	protected static void loopGuiObject1() {
		guiObjects.startScan();
		EWMouseAssociation obj;
		while (guiObjects.next()) {
			obj = guiObjects.getCurrent();
			if (obj == null || obj.object == null)
				continue;
			if (testCollision(obj.object)) {
				guiMode = true;
				if (!obj.over)
					over(obj);
				int nx = getX() - (int) obj.object.getXOnScreen();
				int ny = getY() - (int) obj.object.getYOnScreen();
				obj.object.onOver(nx, ny);
				if (someOne) {
					obj.object.onDown(nx, ny);
					obj.down = true;
					if (oldSomeOne && (oldx != x || oldy != y)) {
						obj.object.onDrag(x - oldx, y - oldy);
					}
				}
				if (oldSomeOne && !someOne) {
					if (obj.down) {
						obj.down = false;
						obj.object.onClick(nx, ny);
					}
					obj.object.onRelease(nx, ny);
				}
				// obj.object.onClick(nx, ny);
			} else if (obj.over) {
				notOver(obj);
			}
		}
	}

	private static void over(EWMouseAssociation ass) {
		ass.over = true;
		ass.object.onEnter();
	}

	private static void notOver(EWMouseAssociation ass) {
		ass.over = false;
		ass.down = false;
		ass.object.onExit();
	}

	/**
	 * returns the x coordinate of mouse on the Component<br>
	 * <b>the coordinates are relative to the Desktop resolution</b>
	 * 
	 * @return returns the x coordinate of mouse
	 */
	// public synchronized static int getX() {
	// return x;
	// }
	/**
	 * returns the y coordinate of mouse on the Component<br>
	 * <b>the coordinates are relative to the Desktop resolution</b>
	 * 
	 * @return returns the y coordinate of mouse
	 */
	// public synchronized static int getY() {
	// return y;
	// }
	/**
	 * returns the x coordinate of mouse on the Camera<br>
	 * <b>the coordinates are relative to the game's Camera system</b>
	 * 
	 * @return returns the x coordinate of mouse
	 */
	public synchronized static int getX() {
		Camera camera = StaticRef.getCamera();
		return (int) ((float) x * camera.getZoom2D() * camera.getWidthFactor());
	}

	/**
	 * returns the y coordinate of mouse on the Camera<br>
	 * <b>the coordinates are relative to the game's Camera system</b>
	 * 
	 * @return returns the y coordinate of mouse
	 */
	public synchronized static int getY() {
		Camera camera = StaticRef.getCamera();
		return (int) ((float) y * camera.getZoom2D() * camera.getHeightFactor());
	}

	/**
	 * returns the x coordinate of mouse in the world<br>
	 * <b>the coordinates are relative to the world's coordinate system</b>
	 * 
	 * @return returns the x coordinate of mouse in the world
	 */
	public synchronized static float getXinWorld() {
		Camera camera = StaticRef.getCamera();
		return ((float) x * camera.getZoom2D() * camera.getWidthFactor() + camera
				.getx());
	}

	/**
	 * returns the y coordinate of mouse in the world<br>
	 * <b>the coordinates are relative to the world's coordinate system</b>
	 * 
	 * @return returns the y coordinate of mouse in the world
	 */
	public synchronized static float getYinWorld() {
		Camera camera = StaticRef.getCamera();
		return ((float) y * camera.getZoom2D() * camera.getHeightFactor() + camera
				.gety());
	}

	/**
	 * 
	 * @return returns if the left button is down or not
	 */
	public static boolean isLeftDown() {
		return left;
	}

	/**
	 * 
	 * @return returns if the right button is down or not
	 */
	public static boolean isRightDown() {
		return right;
	}

	/**
	 * 
	 * @return returns if the middle button is down or not
	 */
	public static boolean isMiddleDown() {
		return middle;
	}

	/**
	 * 
	 * @return returns if the left button is up or not
	 */
	public static boolean isLeftUp() {
		return !left;
	}

	/**
	 * 
	 * @return returns if the right button is up or not
	 */
	public static boolean isRightUp() {
		return !right;
	}

	/**
	 * 
	 * @return returns if the middle button is up or not
	 */
	public static boolean isMiddleUp() {
		return !middle;
	}

	/**
	 * 
	 * @returns if the left button is released or not
	 */
	public static boolean isLeftReleased() {
		return oldLeft && !left;
	}

	/**
	 * 
	 * @returns if the middle button is released or not
	 */
	public static boolean isMiddleReleased() {
		return oldMiddle && !middle;
	}

	/**
	 * 
	 * @returns if the left button is released or not
	 */
	public static boolean isRightReleased() {
		return oldRight && !right;
	}

	/**
	 * 
	 * @returns if the left button is pressed or not
	 */
	public static boolean isLeftPressed() {
		return !oldLeft && left;
	}

	/**
	 * 
	 * @returns if the middle button is pressed or not
	 */
	public static boolean isMiddlePressed() {
		return !oldMiddle && middle;
	}

	/**
	 * returns the state of the Weel's mouse
	 * 
	 * @returns the state of the Weel's mouse
	 */
	public static boolean isRightPressed() {
		return !oldRight && right;
	}

	public static int getWheelState() {
		if (org.lwjgl.input.Mouse.isCreated())
			return org.lwjgl.input.Mouse.getDWheel();
		Utility
				.error(
						"getWheelState() called while the game engine's mouse isn't crated yet",
						"Mouse.getWheelState");
		return 0;
	}

	/**
	 * only with LWJGL
	 * 
	 * @return returns if the indicates button number is down or not
	 */
	public static boolean isButtonDown(int number) {
		if (org.lwjgl.input.Mouse.isCreated())
			return org.lwjgl.input.Mouse.isButtonDown(number);
		Utility
				.error(
						"isButtonDown(int) called while the game engine's mouse isn't crated yet",
						"Mouse.isButtonDown(int)");
		return false;
	}

	// public synchronized static void sendEvent(int[] values, int type) {
	// switch (type) {
	// case 0:
	// bx = values[0];
	// by = values[1];
	// break;
	// case 1: // pressed
	// if (values[0] != 0)
	// bleft = true;
	// if (values[1] != 0)
	// bmiddle = true;
	// if (values[2] != 0)
	// bright = true;
	// break;
	// case 2: // released
	// if (values[0] != 0)
	// bleft = false;
	// if (values[1] != 0)
	// bmiddle = false;
	// if (values[2] != 0)
	// bright = false;
	// break;
	// }
	// }

	// TODO: SPEED UP THE CODE (removing the brute force searching)
	/**
	 * returns a list of IPlain2D that are under the cursor<br>
	 * caution: this is slow.. we should speed up the code (removing the brute
	 * force testing)
	 * 
	 * @return a list of IPlain2D that are under the cursor
	 */
	@Deprecated
	public static BaseList getOver() {
		// loop();
		BaseList<IDrawing> bl = new BaseList<IDrawing>();
		IPlain2D p;
		IDrawing b;
		for (int i = 0; i < StaticRef.layers.length; ++i) {
			StaticRef.layers[i].startScan();
			while (StaticRef.layers[i].next()) {
				b = StaticRef.layers[i].getCurrent();
				if (b instanceof IPlain2D) {
					p = (IPlain2D) b;
					if (testRectangleCollision(p)) {
						bl.add(b);
					}
				}
			}
		}
		if (bl.size() > 0)
			return bl;
		return null;
	}

	public static ArrayList<IPlain2D> getOver(ArrayList<IPlain2D> list) {
		// loop();
		ArrayList<IPlain2D> bl = new ArrayList<IPlain2D>(list.size() / 2);
		IPlain2D p;
		for (int i = 0; i < list.size(); ++i) {
			p = list.get(i);
			if (testRectangleCollision(p)) {
				// System.out.println("found");
				bl.add(p);
			}
		}

		if (bl.size() > 0)
			return bl;
		return null;
	}

	/**
	 * tests a collision with a IPlain2D and returns if this is under the
	 * cursor.
	 * 
	 * @return returns if the IPlain2D is under the cursor
	 */
	public synchronized static boolean testRectangleCollision(IPlain2D p) {
		final int xcam = getX();
		final int ycam = getY();
		final Camera camera = StaticRef.getCamera();
		final float zoomCamera = camera.getZoom2D();
		if (xcam >= p.getXOnScreen() && ycam >= p.getYOnScreen()
				&& xcam < (p.getWidth() - 1) + p.getXOnScreen()
				&& ycam < (p.getHeight() - 1) + p.getYOnScreen()) {
			return true;
		}
		return false;
	}

	/**
	 * tests a collision with a ISpriteColl and returns if this has got a
	 * precise pixel collision with the cursor.
	 * 
	 * @returns returns true if the ISpriteColl has got a precise pixel
	 *          collision with the mouse
	 */
	public synchronized static <T extends IMaskerable & IPlain2D> boolean testCollision(
			T p) {
		Mask mask;
		if ((mask = p.getMask()) == null) {
			if (testRectangleCollision((IPlain2D) p))
				return true;
			return false;
		}
		if (!testRectangleCollision((IPlain2D) p))
			return false;
		if (mask.full)
			return true;
		int nx = getX() - (int) p.getXOnScreen();
		int ny = getY() - (int) p.getYOnScreen();
		if (mask.width != (float) p.getWidth()
				/ StaticRef.getCamera().getZoom2D()
				|| mask.height != (float) p.getHeight()
						/ StaticRef.getCamera().getZoom2D()) {
			final float factorX = ((float) p.getWidth() / StaticRef.getCamera()
					.getZoom2D())
					/ mask.width;
			final float factorY = ((float) p.getHeight() / StaticRef
					.getCamera().getZoom2D())
					/ mask.height;
			if (nx / factorX >= mask.width || ny / factorY >= mask.height)
				return false;
			return mask.mask[(int) (nx / factorX)][(int) (ny / factorY)];
		}
		return mask.mask[nx][ny];
	}

	/**
	 * adds an IClickable object to the mouse; note
	 * 
	 * @param plain
	 *            the object
	 */
	public synchronized static void add(IClickable plain) {
		EWMouseAssociation ass = new EWMouseAssociation(plain);
		objects.add(ass);
		ass.object.getUsedLists().add(objects);
	}

	/**
	 * removes a Gui IClickable object from the mouse
	 * 
	 * @param plain
	 *            the object
	 */
	public synchronized static void remove(IClickable plain) {
		objects.remove((IClickable) plain);
	}

	/**
	 * adds a Gui IClickable object to the mouse; note
	 * 
	 * @param plain
	 *            the object
	 */
	public synchronized static void addGuiObject(IContainer plain) {
		EWMouseAssociation ass = new EWMouseAssociation(plain);
		guiObjects.add(ass);
		ass.object.getUsedLists().add(guiObjects);
	}

	/**
	 * removes an IClickable object from the mouse
	 * 
	 * @param plain
	 *            the object
	 */
	public synchronized static void removeGuiObject(IClickable plain) {
		guiObjects.remove((IClickable) plain);
	}

	/**
	 * hide the mouse
	 * 
	 */
	public synchronized static void hide() {
		if (useLwjgl) {
			org.lwjgl.input.Mouse.setGrabbed(true);
		} else {
			Dimension d;
			Toolkit tk;
			if ((d = (tk = Toolkit.getDefaultToolkit()).getBestCursorSize(1, 1)).width == 0
					|| d.height == 0)
				d.width = d.height = 1;
			BufferedImage Bimage = new BufferedImage(d.width, d.height, 2);
			java.awt.Cursor cursor = tk.createCustomCursor(Bimage, new Point(0,
					0), "");
			StaticRef.core.getContainer().setCursor(cursor);
		}
	}

	/**
	 * shows the mouse
	 * 
	 */
	public synchronized static void show() {
		if (useLwjgl) {
			org.lwjgl.input.Mouse.setGrabbed(false);
		} else {
			StaticRef.core.getContainer().setCursor(
					java.awt.Cursor.getDefaultCursor());
		}
	}

	/**
	 * sets a texture image to as the native cursor. note that the texture will
	 * be converted to ARGB format from the RGBA original format. I advise you
	 * to make a copy of this texture if you should be ute the same texture with
	 * sprites or other stuff.
	 * 
	 * @param image
	 *            the image to use as cursor image
	 */
	public synchronized static void setCursor(ITexture image) {
		if (!useLwjgl) {
			Utility
					.error(
							"this function don't works in the Swing/Awt+OpenGL combination!",
							"Mouse.setCursor(ITexture)");
			return;
		}
		if (image.getWidth() > 32 || image.getHeight() > 32) {
			Utility.error("The image of cursor should be a 32x32 image",
					"EWMouse.setCursor(Texture)");
		}

		// ---------- to ARGB format --------
		byte r, g, b, a;
		ByteBuffer bb = image.getData();
		for (int i = image.getHeightHW() * image.getHeightHW() - 1; i >= 0; --i) {
			bb.mark();
			r = bb.get(); // red
			g = bb.get(); // green
			b = bb.get(); // blue
			a = bb.get(); // alpha
			bb.reset();
			bb.put(a); // alpha
			bb.put(r); // red
			bb.put(g); // green
			bb.put(b); // blue
		}
		image.setData(bb);
		// --------- end to ARGB format ------
		try {
			Cursor cursor = new Cursor(image.getWidth(), image.getHeight(), 0,
					image.getHeight() - 1, 1, image.getData().asIntBuffer(),
					null);
			org.lwjgl.input.Mouse.setNativeCursor(cursor);
		} catch (LWJGLException e) {
			Utility.error("Error on setCursor(Texture) in EWMouse", e);
		}
	}

	public static boolean isGuiMode() {
		return guiMode;
	}

	public static void setGuiMode(boolean guiMode) {
		Mouse.guiMode = guiMode;
	}

}

class EWMouseList extends BaseList<EWMouseAssociation> {

	private static final long serialVersionUID = 7209196555027509593L;

	public EWMouseList() {
		super(true);
		type.set("$_MOUSELIST");
	}

	public void remove(IBaseObject obj) {
		// remove(Lista.indexOf(obj));
		obj.getUsedLists().remove(this);
		// int index = list.indexOf(obj);
		int index = -1;
		for (int i = size() - 1; i >= 0; --i) {
			if (get(i).object.equals(obj)) {
				index = i;
				get(i).getUsedLists().remove(this);
				break;
			}
		}
		if (index == -1) {
			System.out.println("object not found!");
			new Exception().printStackTrace();
		}
		list.remove(index);
		if (indexScan > index) { // 0.1.8
			--indexScan;
			// if (stack != null && stackIndex > -1)
		}
		if (stack != null)
			testDecIndex(index);
	}

	/** remove the sprite from the list */
	public void remove(int index) {
		list.get(index).object.getUsedLists().remove(this);
		list.remove(index);
		if (indexScan > index) {// 0.1.8
			--indexScan;
			// if (stack != null && stackIndex > -1)
		}
		if (stack != null)
			testDecIndex(index);
	}

	/** remove the sprite from the list */
	public void remove(IClickable obj) {
		// remove(Lista.indexOf(obj));
		obj.getUsedLists().remove(this);
		// int index = list.indexOf(obj);
		int index = -1;
		for (int i = size() - 1; i >= 0; --i) {
			if (get(i).equals(obj)) {
				index = i;
				break;
			}
		}
		if (index == -1) {
			System.out.println("object not found!");
			new Exception().printStackTrace();
		}
		list.remove(index);
		if (indexScan > index) { // 0.1.8
			--indexScan;
			// if (stack != null && stackIndex > -1)
		}
		if (stack != null)
			testDecIndex(index);
	}

	public EWMouseAssociation getAssociation(IClickable obj) {
		EWMouseAssociation ass;
		for (int i = size() - 1; i >= 0; --i) {
			if ((ass = get(i)).equals(obj)) {
				return ass;
			}
		}
		// not found
		return null;
	}
}
